/*
一.打印bash命令行提示符
1.获取用户名 主机名 当前路径 GetUsrName GetHostName GetPwd
 利用getenv获取环境变量
 判断是否成功

2.  利用cpp封装提示符的获取和打印
    将第一步获取的参数利用snprintf格式化输出到cmd_prompt中
    然后再由定义的printcommand()调用cmd_prompt（）完成打印

  二. 利用cpp封装命令的获取和打印
    1.定义GetCommandLine()封装提示父的输入和判断
    2.如果GetCommandLine()返回为True 则进行命令打印
    3.判断用户输入是否输入成功 成共打印循环 失败直接返回循环

二.命令行解析
   1.命令行切割（构建命令行参数表）
     将命令装入函数指针数组里 利用strtok()
     统计命令行参数个数时g_argv时要注意不要把\0的个数也算进去

s
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>

#define MAX_NUMS 1024
#define PROMPT "[%s@%s:~%s] "
#define DELIM " " // 定义分隔符

// 定义关于重定向的宏
#define NONE_REDIR 0
#define INPUT_REDIR 1  //< 输入到显示屏文件!
#define OUTPUT_REDIR 2 //>
#define APPEND_REDIR 3 //>>

// 关于重定向的文件名全局字符串
std::string FileName;
int redir = 0;
// 定义全局变量
char command_prompt[MAX_NUMS] = ""; // 定义命令行提示符串(定长大小的数组必须提前开好空间) 因此sizeo后的值也是MAX_NUMS
char command_line[MAX_NUMS];        // 命令行储存
static int g_argc = 0;              // 命令行字符数量
static char *g_argv[MAX_NUMS];      // 命令行储存

// 获取当前用户
char *GetUserName()
{
   char *user = getenv("USER");
   // 判断是否获取成功
   return (user == NULL) ? NULL : user;
}

// 主机名
char *GetHostName()
{
   char *host = getenv("HOSTNAME");
   // 判断是否获取成功
   return (host == NULL) ? NULL : host;
}

// 当前路径
char *GetPwd()
{
   char *pwd = getenv("PWD");
   // 判断是否获取成功
   return (pwd == NULL) ? NULL : pwd;
}

char *GetHome()
{
   char *home = getenv("HOME");
   return home == NULL ? NULL : home;
}

// 封装命令行提示符
void cmd_prompt()
{
   // 封装命令行提示数组
   // snprintf 必须遵循 (缓冲区, 缓冲区大小, 格式字符串, 变量...) 的参数顺序
   snprintf(command_prompt, sizeof(command_prompt), PROMPT, GetUserName(), GetHostName(), GetPwd());
}

// 打印命令行提示符
void printcommand()
{
   cmd_prompt();
   // 以PROMPT 格式打印命令行提示符(printf需要添加格式符)
   printf("%s", command_prompt);
   // 将其立刻回显到屏幕
   fflush(stdout);
}

// 用户输入指令获取(要以\n作为分割符)
bool GetCommandLine()
{
   /*
    1.利用fgets输入其标准输入流
    2.如果一个字符未输入 则返回false其主程序让用户重新输入 ; 如果输入成功，则返回true
   */

   // fgets(argv, sizeof(argv), stdin) ;//
   // g_argc = sizeof(argv);

   char *c = fgets(command_line, sizeof(command_line), stdin);
   if (c == NULL)
      return false;

   // 判断用户输入字符个数并打印输入字符
   int c_nums = strlen(command_line);
   if (c_nums == 1)
      return false;

   // 将字符串最后一位置0 防止打印完换行
   command_line[c_nums - 1] = '\0';
   // printf("%s",out);
   return true;
}

// 命令行分析()
bool CommandParse()
{
   // 将char out[]中每个有效元素拿出（\0前）依次以 ” “ 分割放入g_argv[]中
   g_argv[g_argc] = strtok(command_line, DELIM);
   g_argc++;
   // 后续调用从上次” “的下一个位置开始
   while (g_argv[g_argc] = strtok(NULL, DELIM))
   {
      g_argc++;
   }

   // 判断命令行是否有效
   if (g_argc == 0)
   {
      return false;
   }

   else
   {
      return true;
   }
}

// 判断空格
int TrimSpace(char cmd_line[], int end)
{
   while (cmd_line[end] == ' ')
   {
      end++;
   }

   return end;
}

// 重定向分析
void RedirCheck(char cmd_line[], std::string &file_name)
{
   // 每轮重定向前将重定向方式和文件数组置空
   redir = NONE_REDIR;
   // file_name = nullptr;  std::string不能赋值为 nullptr
   file_name.clear();
   // 遍历字符串，遇见重定向符号就以0划分
   int start = 0;
   int end = strlen(cmd_line) - 1;
   // 从后向前开始遍历
   while (start < end)
   {
      // 输入重定向 <(从键盘读取)
      if (cmd_line[end] == '<')
      {
         // 进行分割
         cmd_line[end++] = '\0';
         // 判断空格
         end = TrimSpace(cmd_line, end);
         // 将文件名放入file_name
         //  file_name.push_back(cmd_line[end]);
         printf("%d\n", end);
         file_name = cmd_line + end;
         // 设置状态
         redir = INPUT_REDIR;
         break;
      }

      else if (cmd_line[end] == '>')
      {

         if (cmd_line[end - 1] == '>')
         { //  >>
            // 进行分割
            cmd_line[end - 1] = '\0';
            // 设置状态
            redir = APPEND_REDIR;
         }

         else
         { // >
            redir = OUTPUT_REDIR;
         }

         cmd_line[end++] = '\0';
         // 判断空格
         end = TrimSpace(cmd_line, end);
         // 将文件名放入file_name
         file_name = cmd_line + end;
         break;
      }
      end--;
   }
}

// 判断命令是否为内建命令 如果内建命令则让父进程执行 并返回true
bool BuiltinCommand()
{
   // 先判断 g_argv[0] 是否为空（避免空指针访问）
   if (g_argv[0] == NULL)
   {
      return false;
   }

   // 判断
   std::string cmd = g_argv[0];
   if (cmd == "cd")
   {
      const char *target_dir = NULL; // 目标目录路径

      // 场景1 g_argv[1] == NULL
      if (g_argv[1] == NULL)
      {
         target_dir = GetHome();
      }

      // 场景2：cd 有参数（g_argv[1] 不为空）→ 切换到指定目录
      else
      {
         target_dir = g_argv[1];
      }

      // 调用 chdir 切换目录，判断是否成功
      if (chdir(target_dir) < 0)
      {
         // 打印错误原因（如 "没有该文件或目录"、"权限不够" 等）
         perror("cd 失败"); // 自动关联 errno 对应的错误信息
         return false;      // 内建命令执行失败，返回false
      }

      else
      {
         // 切换成功（可选：更新环境变量 PWD，让提示符显示最新路径）
         char current_pwd[MAX_NUMS];
         if (getcwd(current_pwd, sizeof(current_pwd)) != NULL)
         {
            setenv("PWD", current_pwd, 1); // 1 表示覆盖原有环境变量
         }
         return true;
      }
   }
   // 非内建返回false
   return false;
}

// 程序替换
void Execute()
{
   umask(0);
   pid_t id = fork();
   if (id == 0)
   { // 重定向分析
      if (redir == INPUT_REDIR)
      { // < 1
         // 输入重定向 以读的方式打开
         int fd = open(FileName.c_str(), O_RDONLY); // fd返回的是文件描述符
         if (fd < 0)
            exit(1);
         // 进行重定向 dunp2
         dup2(fd, 0); // 从stdin(0) 到文件中
         // 关闭文件
         close(fd);
      }

      else if (redir == OUTPUT_REDIR)
      { // > 2
         // 输入重定向 以写的方式打开
         int fd = open(FileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); // fd返回的是文件描述符
         if (fd < 0)
            exit(1);
         // 进行重定向 dunp2
         dup2(fd, 1); // 从stdout(1) 到文件中 将1号数组里的文件指针指向
         // 关闭文件
         close(fd);
      }

      else if (redir == APPEND_REDIR)
      { // >> 3
         // 输入重定向 以写的方式打开
         int fd = open(FileName.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666); // fd返回的是文件描述符
         if (fd < 0)
            exit(1);
         // 进行重定向 dunp2
         dup2(fd, 1); // 从stdout(1) 到文件中 将1号数组里的文件指针指向
         // 关闭文件
         close(fd);
      }

      // 让子进程替换父进程
      execvp(g_argv[0], g_argv);
      exit(1);
   }
   // 子进程回收
   waitpid(-1, NULL, 0);
}

int main()
{
   while (1)
   {

      // 每次调用开始时重置参数计数和数组
      g_argc = 0;
      memset(g_argv, 0, sizeof(g_argv)); // 同时清空参数数组，避免残留上次数据

      // 提示符打印
      printcommand();

      // 获取用户输入指令
      if (!GetCommandLine())
      { ////当用户输   入失败(或未输入）则在使其重更新输入
         continue;
      }

      // 对用户命令进行重定向分析
      RedirCheck(command_line, FileName);
      printf("命令是：%d ,文件名是：%s", redir, FileName.c_str());

      // 对用户命令行进行分析(切割) 'l','s',....
      if (!CommandParse())
         continue;

      // 如果非内建命令则让子进程替换
      if (!BuiltinCommand())
         // 程序替换
         // printf("%d ", redir);
         Execute();
   }

   return 0;
}
